/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package java.util.jar;

import com.jtransc.io.JTranscIoTools;

import java.io.File;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

/**
 * {@code JarFile} is used to read jar entries and their associated data from
 * jar files.
 *
 * @see JarInputStream
 * @see JarEntry
 */
public class JarFile extends ZipFile {

	/**
	 * The MANIFEST file name.
	 */
	public static final String MANIFEST_NAME = "META-INF/MANIFEST.MF";

	// The directory containing the manifest.
	static final String META_DIR = "META-INF/";

	// The manifest after it has been read from the JAR.
	private Manifest manifest;

	// The entry for the MANIFEST.MF file before it is read.
	private ZipEntry manifestEntry;

	private boolean closed = false;

	static final class JarFileInputStream extends FilterInputStream {
		private long count;

		private ZipEntry zipEntry;

		private boolean done = false;

		JarFileInputStream(InputStream is, ZipEntry ze) {
			super(is);
			zipEntry = ze;
			count = zipEntry.getSize();
		}

		@Override
		public int read() throws IOException {
			if (done) {
				return -1;
			}
			if (count > 0) {
				int r = super.read();
				if (r != -1) {
					count--;
				} else {
					count = 0;
				}
				if (count == 0) {
					done = true;
				}
				return r;
			} else {
				done = true;
				return -1;
			}
		}

		@Override
		public int read(byte[] buffer, int byteOffset, int byteCount) throws IOException {
			if (done) {
				return -1;
			}
			if (count > 0) {
				int r = super.read(buffer, byteOffset, byteCount);
				if (r != -1) {
					int size = r;
					if (count < size) {
						size = (int) count;
					}
					count -= size;
				} else {
					count = 0;
				}
				if (count == 0) {
					done = true;
				}
				return r;
			} else {
				done = true;
				return -1;
			}
		}

		@Override
		public int available() throws IOException {
			if (done) {
				return 0;
			}
			return super.available();
		}

		@Override
		public long skip(long byteCount) throws IOException {
			return JTranscIoTools.skipByReading(this, byteCount);
		}
	}

	/**
	 * Create a new {@code JarFile} using the contents of the specified file.
	 *
	 * @param file the JAR file as {@link File}.
	 * @throws IOException If the file cannot be read.
	 */
	public JarFile(File file) throws IOException {
		this(file, true);
	}

	/**
	 * Create a new {@code JarFile} using the contents of the specified file.
	 *
	 * @param file   the JAR file as {@link File}.
	 * @param verify if this JAR file is signed whether it must be verified.
	 * @throws IOException If the file cannot be read.
	 */
	public JarFile(File file, boolean verify) throws IOException {
		super(file);
		if (verify) {
		}
		readMetaEntries();
	}

	/**
	 * Create a new {@code JarFile} using the contents of file.
	 *
	 * @param file   the JAR file as {@link File}.
	 * @param verify if this JAR filed is signed whether it must be verified.
	 * @param mode   the mode to use, either {@link ZipFile#OPEN_READ OPEN_READ} or
	 *               {@link ZipFile#OPEN_DELETE OPEN_DELETE}.
	 * @throws IOException If the file cannot be read.
	 */
	public JarFile(File file, boolean verify, int mode) throws IOException {
		super(file, mode);
		if (verify) {
		}
		readMetaEntries();
	}

	/**
	 * Create a new {@code JarFile} from the contents of the file specified by
	 * filename.
	 *
	 * @param filename the file name referring to the JAR file.
	 * @throws IOException if file name cannot be opened for reading.
	 */
	public JarFile(String filename) throws IOException {
		this(filename, true);
	}

	/**
	 * Create a new {@code JarFile} from the contents of the file specified by
	 * {@code filename}.
	 *
	 * @param filename the file name referring to the JAR file.
	 * @param verify   if this JAR filed is signed whether it must be verified.
	 * @throws IOException If file cannot be opened or read.
	 */
	public JarFile(String filename, boolean verify) throws IOException {
		super(filename);
		if (verify) {
		}
		readMetaEntries();
	}

	/**
	 * Return an enumeration containing the {@code JarEntrys} contained in this
	 * {@code JarFile}.
	 *
	 * @return the {@code Enumeration} containing the JAR entries.
	 * @throws IllegalStateException if this {@code JarFile} is closed.
	 */
	@Override
	public Enumeration<JarEntry> entries() {
		class JarFileEnumerator implements Enumeration<JarEntry> {
			Enumeration<? extends ZipEntry> ze;

			JarFile jf;

			JarFileEnumerator(Enumeration<? extends ZipEntry> zenum, JarFile jf) {
				ze = zenum;
				this.jf = jf;
			}

			public boolean hasMoreElements() {
				return ze.hasMoreElements();
			}

			public JarEntry nextElement() {
				JarEntry je = new JarEntry(ze.nextElement());
				je.parentJar = jf;
				return je;
			}
		}
		return new JarFileEnumerator(super.entries(), this);
	}

	/**
	 * Return the {@code JarEntry} specified by its name or {@code null} if no
	 * such entry exists.
	 *
	 * @param name the name of the entry in the JAR file.
	 * @return the JAR entry defined by the name.
	 */
	public JarEntry getJarEntry(String name) {
		return (JarEntry) getEntry(name);
	}

	/**
	 * Returns the {@code Manifest} object associated with this {@code JarFile}
	 * or {@code null} if no MANIFEST entry exists.
	 *
	 * @return the MANIFEST.
	 * @throws IOException           if an error occurs reading the MANIFEST file.
	 * @throws IllegalStateException if the jar file is closed.
	 * @see Manifest
	 */
	public Manifest getManifest() throws IOException {
		if (closed) {
			throw new IllegalStateException("JarFile has been closed");
		}
		if (manifest != null) {
			return manifest;
		}
		try {
			try (InputStream is = super.getInputStream(manifestEntry)) {
				manifest = new Manifest(is, false);
			}
			manifestEntry = null;  // Can discard the entry now.
		} catch (NullPointerException e) {
			manifestEntry = null;
		}
		return manifest;
	}

	/**
	 * Called by the JarFile constructors, this method reads the contents of the
	 * file's META-INF/ directory and picks out the MANIFEST.MF file and
	 * verifier signature files if they exist. Any signature files found are
	 * registered with the verifier.
	 *
	 * @throws IOException if there is a problem reading the jar file entries.
	 */
	private void readMetaEntries() throws IOException {
		// Get all meta directory entries
		ZipEntry[] metaEntries = getMetaEntriesImpl();
		if (metaEntries == null) {
			return;
		}

		boolean signed = false;

		for (ZipEntry entry : metaEntries) {
			String entryName = entry.getName();
			// Is this the entry for META-INF/MANIFEST.MF ?
			if (manifestEntry == null && entryName.equalsIgnoreCase(MANIFEST_NAME)) {
				manifestEntry = entry;
				// If there is no verifier then we don't need to look any further.
				break;
			} else {
				// Is this an entry that the verifier needs?
			}
		}

		// If there were no signature files, then no verifier work to do.
	}

	private static boolean endsWithIgnoreCase(String s, String suffix) {
		return s.regionMatches(true, s.length() - suffix.length(), suffix, 0, suffix.length());
	}

	/**
	 * Return an {@code InputStream} for reading the decompressed contents of
	 * ZIP entry.
	 *
	 * @param ze the ZIP entry to be read.
	 * @return the input stream to read from.
	 * @throws IOException if an error occurred while creating the input stream.
	 */
	@Override
	public InputStream getInputStream(ZipEntry ze) throws IOException {
		if (manifestEntry != null) {
			getManifest();
		}

		InputStream in = super.getInputStream(ze);
		if (in == null) {
			return null;
		}
		if (ze.getSize() == -1) {
			return in;
		}
		return new JarFileInputStream(in, ze);
	}

	/**
	 * Return the {@code JarEntry} specified by name or {@code null} if no such
	 * entry exists.
	 *
	 * @param name the name of the entry in the JAR file.
	 * @return the ZIP entry extracted.
	 */
	@Override
	public ZipEntry getEntry(String name) {
		ZipEntry ze = super.getEntry(name);
		if (ze == null) {
			return ze;
		}
		JarEntry je = new JarEntry(ze);
		je.parentJar = this;
		return je;
	}

	/**
	 * Returns all the ZipEntry's that relate to files in the
	 * JAR's META-INF directory.
	 *
	 * @return the list of ZipEntry's or {@code null} if there are none.
	 */
	private ZipEntry[] getMetaEntriesImpl() {
		List<ZipEntry> list = new ArrayList<ZipEntry>(8);
		Enumeration<? extends ZipEntry> allEntries = entries();
		while (allEntries.hasMoreElements()) {
			ZipEntry ze = allEntries.nextElement();
			if (ze.getName().startsWith(META_DIR)
				&& ze.getName().length() > META_DIR.length()) {
				list.add(ze);
			}
		}
		if (list.size() == 0) {
			return null;
		}
		ZipEntry[] result = new ZipEntry[list.size()];
		list.toArray(result);
		return result;
	}

	/**
	 * Closes this {@code JarFile}.
	 *
	 * @throws IOException if an error occurs.
	 */
	@Override
	public void close() throws IOException {
		super.close();
		closed = true;
	}
}
